package sm.dbdict;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import sm.dbdict.def.DBDict;
import sm.dbdict.def.Field;
import sm.dbdict.def.Key;
import sm.dbdict.def.SqlFieldOptions;
import sm.dbdict.def.SqlIndex;
import sm.dbdict.def.SqlTables;
import sm.def.KeyType;
import sm.def.Type;
import sm.util.Util;

public class DBDictUtil {

	/**
	 * get path of field
	 * 
	 * @param field
	 * @return
	 */
	public static String getFieldPath(Field field) {
		String path = field.getName();
		Field parent = field.getParentField();

		if (parent != null) {
			path = getFieldPath(parent) + "," + path;
		}
		return path;
	}

	/**
	 * get its array parent, return null for not array field
	 * 
	 * @param field
	 * @return
	 */
	public static Field getArrayParent(Field field) {
		Field arrayParent = null;
		Field parent = field.getParentField();
		if (parent != null) {
			if (parent.getType() == Type.ARRAY_TYPE) {
				arrayParent = parent;
			} else {
				arrayParent = getArrayParent(parent);
			}
		}
		return arrayParent;
	}

	/**
	 * get array path and array field path e.g.: permission.permission.kmprofile
	 * => kmprofile
	 * 
	 * @param field
	 * @return
	 */
	public static String getArrayFieldPath(Field field, Field arrayParent) {
		String path = "";
		Field parent = field.getParentField();
		if (parent != null && !parent.equals(arrayParent)) {
			path = field.getName();
			String parentPath = getArrayFieldPath(parent, arrayParent);
			if (parentPath.length() > 0) {
				path = parentPath + "," + path;
			}
		}

		return path;
	}

	/**
	 * get field by path
	 * 
	 * @param path
	 * @param fields
	 * @return
	 */
	public static Field getField(String path, List<Field> fields) {
		for (Field field : fields) {
			if (path.equals(getFieldPath(field))) {
				return field;
			}
		}
		return null;
	}

	/**
	 * get field by name, first occur
	 * 
	 * @param name
	 * @param fields
	 * @return
	 */
	public static Field getFieldByName(String name, List<Field> fields) {
		for (Field field : fields) {
			if (name.equals(field.getName())) {
				return field;
			}
		}
		return null;
	}

	/**
	 * get field by name, index and level, first occur
	 * 
	 * @param name
	 * @param index
	 * @param level
	 * @param fields
	 * @return
	 */
	public static Field getFieldByNameIndexLevel(String name, int index,
			int level, List<Field> fields) {
		for (Field field : fields) {
			if (index == field.getIndex() && level == field.getLevel()
					&& name.equals(field.getName())) {
				return field;
			}
		}
		return null;
	}

	public static void removeKey(DBDict dbdict, Key key) {
		for (ListIterator<Key> it = dbdict.getKey().listIterator(); it
				.hasNext();) {
			Key k = it.next();
			if (k.equals(key)) {
				it.remove();
			}
		}
	}

	/**
	 * get unique keys in dbdict
	 * 
	 * @param dbdict
	 * @return
	 */
	public static List<Key> getUniqueKeys(DBDict dbdict) {
		List<Key> uniqueKeys = new ArrayList<Key>();
		for (Key key : dbdict.getKey()) {
			int flags = key.getFlags();
			if (flags == KeyType.UNIQUE.getFlags()
					|| flags == KeyType.NO_DUPLICATES.getFlags()) {
				uniqueKeys.add(key);
			}
		}
		return uniqueKeys;
	}

	/**
	 * get the table defines the key field
	 * 
	 * @param dbdict
	 * @param key
	 * @return
	 */
	public static SqlTables getKeyTable(DBDict dbdict, Key key) {
		SqlTables table = null;
		List<Field> fields = getKeyFields(key, dbdict.getField());
		for (Field field : fields) {
			if (isDBField(field)) {
				table = getTableByAlias(dbdict.getSql_tables(), field
						.getSql_field_options().getSql_table_alias());
				break;
			}
		}
		return table;
	}

	/**
	 * get the primary table
	 * 
	 * @param dbdict
	 * @return
	 */
	public static SqlTables getPrimaryTable(DBDict dbdict) {
		List<SqlTables> tables = dbdict.getSql_tables();
		SqlTables primaryTable = null;
		int tableCount = tables.size();
		if (tableCount > 0) {
			if (tableCount == 1) {
				primaryTable = tables.get(0);
			} else {
				Key primaryKey = getPrimaryKey(dbdict);
				if (primaryKey != null) {
					primaryTable = getKeyTable(dbdict, primaryKey);
				}
				if (primaryTable == null) {
					primaryTable = tables.get(0);
				}
			}
		}

		return primaryTable;

	}

	/**
	 * get primary key
	 * 
	 * @param dbdict
	 * @return
	 */
	public static Key getPrimaryKey(DBDict dbdict) {
		List<Key> uniqueKeys = getUniqueKeys(dbdict);

		Key pk = null;
		int maxIdxCount = 0;
		for (Key uniqueKey : uniqueKeys) {
			int indexCount = uniqueKey.getSqlIndexes().size();
			if (indexCount > maxIdxCount) {
				maxIdxCount = indexCount;
				pk = uniqueKey;
			} else if (indexCount == maxIdxCount && pk != null) {
				// choose the short key
				if (uniqueKey.getName().size() < pk.getName().size()) {
					pk = uniqueKey;
				}
			}
		}
		return pk;
	}

	/**
	 * get identifier of a dbdict, for data comparison
	 * 
	 * @param dbdict
	 * @return
	 */
	public static Key getIdentifier(DBDict dbdict) {
		Key identifier = getPrimaryKey(dbdict);
		if (identifier == null) {
			identifier = dbdict.getKey().get(0);
		}
		return identifier;
	}

	public static String[] getKeyPaths(List<Field> keyFields) {
		int size = keyFields.size();
		String[] keyPaths = new String[size];
		for (int i = 0; i < size; i++) {
			Field keyField = keyFields.get(i);
			String keyPath = getFieldPath(keyField);
			keyPaths[i] = keyPath;
		}
		return keyPaths;
	}

	/**
	 * get the string format of keys
	 * 
	 * @param keys
	 * @return
	 */
	public static String getUniqueKeyStr(List<Key> keys) {
		List<String> uniqueKeysStr = new ArrayList<String>();
		for (Key key : keys) {
			uniqueKeysStr.add(getKeyString(key));
		}

		return uniqueKeysStr.toString();
	}

	/**
	 * get unique key string
	 * 
	 * @param dbdict
	 * @return
	 */
	public static String getUniqueKeyStr(DBDict dbdict) {
		List<Key> uniqueKeys = getUniqueKeys(dbdict);
		return getUniqueKeyStr(uniqueKeys);
	}

	/**
	 * get the string format of key
	 * 
	 * @param key
	 * @return
	 */
	public static String getKeyString(Key key) {
		List<String> names = key.getName();
		Collections.sort(names);
		return Util.formatKey(names.toArray());
	}

	/**
	 * get key related fields
	 * 
	 * @param key
	 * @param fields
	 * @return
	 */
	public static List<Field> getKeyFields(Key key, List<Field> fields) {
		List<Field> keyFields = new ArrayList<Field>();
		for (String name : key.getName()) {
			Field field = getKeyField(name, fields);
			keyFields.add(field);
		}
		return keyFields;
	}

	/**
	 * get the key field
	 * 
	 * @param keyPath
	 * @param fields
	 * @return
	 */
	public static Field getKeyField(String keyPath, List<Field> fields) {
		String[] paths = keyPath.split(",");
		String parent = paths[0];
		Field parentField = getField(parent, fields);

		if (parentField != null && isJoinedArray(parentField)) {
			// joined array
			keyPath = parent + "," + keyPath;
		}

		Field keyField = getField(keyPath, fields);

		if (keyField != null) {
			// map alias field to real field
			keyField = getRealField(fields, keyField);
		}
		return keyField;
	}

	/**
	 * get the real field which the alias field related to
	 * 
	 * @param fields
	 * @param aliasField
	 * @return
	 */
	private static Field getRealField(List<Field> fields, Field aliasField) {
		Field parent = aliasField.getParentField();
		if (parent == null) {
			parent = fields.get(0); // descriptor
		}

		List<Field> subFields = getSubFields(fields, parent);
		List<Field> childFields = getChilds(subFields, parent, true);

		int aliasLevel = aliasField.getLevel();
		int aliasIndex = aliasField.getIndex();
		Field realField = null;
		for (Field child : childFields) {
			int fieldLevel = child.getLevel();
			int fieldIndex = child.getIndex();
			if (fieldLevel == aliasLevel && fieldIndex == aliasIndex) {
				realField = child;
				break;
			}
		}
		return realField;
	}

	/**
	 * check if a field is a primary field
	 * 
	 * @param dbdict
	 * @param field
	 * @return
	 */
	public static boolean isPrimaryField(DBDict dbdict, Field field) {
		boolean isPrimaryField = false;
		Key primaryKey = getPrimaryKey(dbdict);
		List<Field> primaryFields = getKeyFields(primaryKey, dbdict.getField());
		if (primaryFields.contains(field)) {
			isPrimaryField = true;
		} else {
			isPrimaryField = false;
		}
		return isPrimaryField;
	}

	/**
	 * get fields with the same alias
	 * 
	 * @param dbdict
	 * @param tableAlias
	 * @return
	 */
	public static List<Field> getTableFields(List<Field> fields,
			String tableAlias) {
		List<Field> tableFields = new ArrayList<Field>();
		for (Field field : fields) {
			if (isDBField(field)) {
				SqlFieldOptions sqlFieldOptions = field.getSql_field_options();
				if (tableAlias.equals(sqlFieldOptions.getSql_table_alias())) {
					tableFields.add(field);
				}
			}
		}
		return tableFields;
	}

	/**
	 * get columns with the same alias
	 * 
	 * @param fields
	 * @param tableAlias
	 * @return
	 */
	public static List<Field> getTableColumns(List<Field> fields,
			String tableAlias) {
		List<Field> tableFields = new ArrayList<Field>();
		for (Field field : fields) {
			if (isDBColumn(field)) {
				SqlFieldOptions sqlFieldOptions = field.getSql_field_options();
				if (tableAlias.equals(sqlFieldOptions.getSql_table_alias())) {
					tableFields.add(field);
				}
			}
		}
		return tableFields;
	}

	/**
	 * get db fields for a table
	 * 
	 * @param dbdict
	 * @param tableAlias
	 * @return
	 */
	public static List<Field> getDBFields(DBDict dbdict, String tableAlias) {
		List<Field> tableFields = getTableFields(dbdict.getField(), tableAlias);

		Key primaryKey = getPrimaryKey(dbdict);
		if (primaryKey != null) {
			List<Field> primaryFields = getKeyFields(primaryKey,
					dbdict.getField());
			for (Field primaryField : primaryFields) {
				if (!tableFields.contains(primaryField)) {
					tableFields.add(primaryField);
				}
			}
		}

		for (Iterator<Field> it = tableFields.listIterator(); it.hasNext();) {
			Field field = it.next();
			if (!isDBColumn(field)) {
				it.remove();
			}
		}

		return tableFields;
	}

	/**
	 * test if it is a db field
	 * 
	 * @param field
	 * @return
	 */
	public static boolean isDBField(Field field) {
		boolean isDBField = false;
		if (field != null) {
			SqlFieldOptions sqlFieldOptions = field.getSql_field_options();
			if (sqlFieldOptions != null) {
				String alias = sqlFieldOptions.getSql_table_alias();
				if (alias != null && alias.trim().length() > 0) {
					isDBField = true;
				}
			}
		}
		return isDBField;
	}

	/**
	 * has a db column name?
	 * 
	 * @param field
	 * @return
	 */
	public static boolean isDBColumn(Field field) {
		boolean isDBColumn = false;
		if (field != null) {
			SqlFieldOptions sqlFieldOptions = field.getSql_field_options();
			if (sqlFieldOptions != null) {
				String columnName = sqlFieldOptions.getSql_column_name();
				if (columnName != null && columnName.trim().length() > 0) {
					isDBColumn = true;
				}
			}
		}
		return isDBColumn;
	}

	/**
	 * test if a field is a joined array
	 * 
	 * @param field
	 * @return
	 */
	public static boolean isJoinedArray(Field field) {
		return field.getType() == Type.ARRAY_TYPE && isDBField(field)
				&& !isDBColumn(field);
	}

	/**
	 * get all joined array fields
	 * 
	 * @param dbdict
	 * @return
	 */
	public static List<Field> getAllJoinedArrays(DBDict dbdict) {
		List<Field> joinedArrays = new ArrayList<Field>();
		for (Field field : dbdict.getField()) {
			if (isJoinedArray(field)) {
				joinedArrays.add(field);
			}
		}

		return joinedArrays;
	}

	/**
	 * test if the fields are in array table
	 * 
	 * @param fields
	 * @return
	 */
	public static boolean isArrayTable(List<Field> fields) {
		boolean isArrayTable = false;
		for (Field field : fields) {
			if (isJoinedArray(field)) {
				isArrayTable = true;
				break;
			}
		}
		return isArrayTable;
	}

	/**
	 * test if a table alias is an array table
	 * 
	 * @param dbdict
	 * @param alias
	 * @return
	 */
	public static boolean isArrayTable(DBDict dbdict, String alias) {
		return isArrayTable(getTableFields(dbdict.getField(), alias));
	}

	/**
	 * get table by alias
	 * 
	 * @param sqlTables
	 * @param tableAlias
	 * @return
	 */
	public static SqlTables getTableByAlias(List<SqlTables> sqlTables,
			String tableAlias) {
		for (SqlTables sqlTable : sqlTables) {
			if (tableAlias.equals(sqlTable.getSql_table_alias())) {
				return sqlTable;
			}
		}
		return null;
	}

	/**
	 * get table by name
	 * 
	 * @param sqlTables
	 * @param tableName
	 * @return
	 */
	public static SqlTables getTableByName(List<SqlTables> sqlTables,
			String tableName) {
		for (SqlTables sqlTable : sqlTables) {
			if (tableName.equals(sqlTable.getSql_table_name())) {
				return sqlTable;
			}
		}
		return null;
	}

	/**
	 * remove invalid tables
	 * 
	 * @param dbdict
	 */
	public static void removeInvalidTables(DBDict dbdict) {
		for (ListIterator<SqlTables> it = dbdict.getSql_tables().listIterator(); it
				.hasNext();) {
			SqlTables table = it.next();
			List<Field> fields = getTableFields(dbdict.getField(),
					table.getSql_table_alias());
			if (fields == null || fields.size() == 0) {
				it.remove();
			}
		}
	}

	/**
	 * sort key by type
	 * 
	 * @param keys
	 */
	public static void sortKeys(List<Key> keys) {
		Comparator<Key> c = new Comparator<Key>() {
			@Override
			public int compare(Key o1, Key o2) {
				int o1Type = o1.getFlags();
				int o2Type = o2.getFlags();
				if (o1Type == KeyType.UNIQUE.getFlags()) {
					return -1;
				} else if (o1Type == KeyType.NO_DUPLICATES.getFlags()) {
					if (o2Type == KeyType.UNIQUE.getFlags()) {
						return 1;
					} else {
						return -1;
					}
				} else if (o1Type == KeyType.NO_NULLS.getFlags()) {
					if (o2Type == KeyType.UNIQUE.getFlags()
							|| o2Type == KeyType.NO_DUPLICATES.getFlags()) {
						return 1;
					} else {
						return -1;
					}
				} else if (o1Type == KeyType.NULL_AND_DUPLICATES.getFlags()) {
					if (o2Type == KeyType.IR_KEY.getFlags()) {
						return -1;
					} else {
						return 1;
					}
				} else {
					return 1;
				}
			}
		};

		Collections.sort(keys, c);
	}

	/**
	 * remove invalid keys
	 * 
	 * @param dbdict
	 */
	public static void removeInvalidKeys(DBDict dbdict) {
		sortKeys(dbdict.getKey());
		Set<String> keys = new HashSet<String>();
		for (ListIterator<Key> it = dbdict.getKey().listIterator(); it
				.hasNext();) {
			Key key = it.next();
			int type = key.getFlags();

			if (type != KeyType.IR_KEY.getFlags()) {
				String keyStr = getKeyString(key);
				if (keys.contains(keyStr)) {
					it.remove();
				} else {
					keys.add(keyStr);
				}
			}
		}
	}

	/**
	 * get sql index by key and table name
	 * 
	 * @param key
	 * @param tableName
	 * @return
	 */
	public static SqlIndex getSqlIndexes(Key key, String tableName) {
		for (SqlIndex sqlIndex : key.getSqlIndexes()) {
			if (sqlIndex.getTableName().equals(tableName)) {
				return sqlIndex;
			}
		}

		return null;
	}

	/**
	 * set parent field
	 * 
	 * @param dbdict
	 */
	public static void setParentField(DBDict dbdict) {
		Map<Integer, Field> parentFields = new HashMap<Integer, Field>();
		for (Field field : dbdict.getField()) {
			int level = field.getLevel();
			// set parent field
			if (level > 0) // skip descriptor
				parentFields.put(level, field);
			Field parentField = parentFields.get(level - 1);
			field.setParentField(parentField);
		}
	}

	/**
	 * get the max index of structure
	 * 
	 * @param siblings
	 * @return
	 */
	public static int getMaxIndex(List<Field> siblings) {
		int index = 0;
		for (Field sibling : siblings) {
			int siblingIdx = sibling.getIndex();
			if (siblingIdx > index) {
				index = siblingIdx;
			}
		}
		return index;
	}

	/**
	 * get a field's position in the dbdict
	 * 
	 * @param fieldPath
	 * @param dbdict
	 * @return
	 */
	public static int getFieldPosition(String fieldPath, DBDict dbdict) {
		int pos = 0;
		List<Field> fields = dbdict.getField();
		for (int i = 0; i < fields.size(); i++) {
			Field f = fields.get(i);
			String fPath = getFieldPath(f);
			if (Util.equals(fieldPath, fPath)) {
				pos = i;
				break;
			}
		}
		return pos;
	}

	/**
	 * insert a new field with its children, return sub fields' size, insert
	 * this field to the tail of its parent
	 * 
	 * @param dbdict
	 * @param field
	 */
	public static void insertField(DBDict dbdict, Field field,
			List<Field> subFields) {
		List<Field> fields = dbdict.getField();
		Field parentField = field.getParentField();

		String parentPath = null;
		if (parentField != null) {
			parentPath = getFieldPath(parentField);
		} else {
			// descriptor
			parentPath = getFieldPath(fields.get(0));
		}

		// get parent field and its position
		int pos = getFieldPosition(parentPath, dbdict);
		parentField = fields.get(pos);

		List<Field> youngers = getSubFields(fields, parentField);
		// add to the tail
		pos = pos + youngers.size() + 1;

		List<Field> siblings = getChilds(youngers, parentField, false);
		int index = getMaxIndex(siblings);
		field.setIndex(index + 1);

		// insert current field
		fields.add(pos, field);
		if (subFields.size() > 0) {
			fields.addAll(pos + 1, subFields);
		}
	}

	/**
	 * get sub fields of struct
	 * 
	 * @param allFields
	 * @param parent
	 * @param skipAlias
	 * @return
	 */
	public static List<Field> getSubFields(List<Field> allFields, Field parent) {
		List<Field> subFields = new ArrayList<Field>();
		if (!Type.isPrimitiveType(parent.getType())) {
			String path = getFieldPath(parent);
			int level = parent.getLevel();
			boolean isChild = false;
			for (Field field : allFields) {
				int fieldLevel = field.getLevel();
				if (isChild && fieldLevel <= level) {
					break;
				}

				if (isChild) {
					subFields.add(field);
				}

				if (!isChild) {
					String fieldPath = getFieldPath(field);
					if (path.equals(fieldPath)) {
						isChild = true;
					}
				}

			}
		}
		return subFields;
	}

	/**
	 * get childs from sub fields, skip alias field
	 * 
	 * @param subFields
	 * @param field
	 * @param sort
	 * @return
	 */
	public static List<Field> getChilds(List<Field> subFields, Field field,
			boolean sortAndSkipAlias) {
		int level = field.getLevel();
		List<Field> childFields = new ArrayList<Field>();
		for (Field f : subFields) {
			if (f.getLevel() == level + 1) {
				childFields.add(f);
			}
		}

		if (sortAndSkipAlias) {
			String hiddenFieldName = "hiddenfield";
			int maxIndex = 0;
			Map<Integer, Field> indexFields = new HashMap<Integer, Field>();
			for (Field f : childFields) {
				int index = f.getIndex();
				// skip alias
				if (!indexFields.containsKey(index)) {
					if (index > maxIndex) {
						maxIndex = index;
					}
					indexFields.put(index, f);
				}
			}
			childFields = new ArrayList<Field>(maxIndex);
			for (int i = 1; i <= maxIndex; i++) {
				Field f = indexFields.get(i);
				if (f == null) {
					f = new Field();
					f.setName(hiddenFieldName + i);
					f.setType(Type.NUMBER_TYPE);
					f.setLevel(level - 1);
					f.setIndex(i);
				}
				childFields.add(f);
			}
		}
		return childFields;
	}

	/**
	 * get real primitive sub fields
	 * 
	 * @param field
	 * @param subFields
	 * @return
	 */
	public static List<Field> getRealSubFields(Field field,
			List<Field> subFields) {
		List<Field> realFields = new ArrayList<Field>();
		List<Field> childs = getChilds(subFields, field, true);
		for (Field child : childs) {
			int type = child.getType();
			if (Type.isPrimitiveType(type)) {
				realFields.add(child);
			} else {
				List<Field> childSubFields = getSubFields(subFields, child);
				realFields.addAll(getRealSubFields(child, childSubFields));
			}
		}

		return realFields;
	}

	/**
	 * refine the dbdict
	 * 
	 * @param dbdict
	 */
	public static void refineDBDict(DBDict dbdict) {
		// remove null tables
		removeInvalidTables(dbdict);
		// remove duplicated keys
		removeInvalidKeys(dbdict);
	}
}
